import 'dart:math' as math;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

/// Delegate for configuring a [SliverHeader].
abstract class SliverHeaderDelegate {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const SliverHeaderDelegate();

  /// The widget to place inside the [SliverHeader].
  ///
  /// The `context` is the [BuildContext] of the sliver.
  ///
  /// The `shrinkOffset` is the header scroll offset. Zero means the header is fully
  /// expanded, any greater number means that the header has been scrolled by that much.
  ///
  /// The `overlapsContent` argument is true if subsequent slivers (if any) will
  /// be rendered beneath this one, and false if the sliver will not have any
  /// contents below it. Typically this is used to decide whether to draw a
  /// shadow to simulate the sliver being above the contents below it. Typically
  /// this is true when `shrinkOffset` is at its greatest value and false
  /// otherwise, but that is not guaranteed. See [NestedScrollView] for an
  /// example of a case where `overlapsContent`'s value can be unrelated to
  /// `shrinkOffset`.
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);

  /// Whether this delegate is meaningfully different from the old delegate.
  ///
  /// If this returns false, then the header might not be rebuilt, even though
  /// the instance of the delegate changed.
  bool shouldRebuild(covariant SliverHeaderDelegate oldDelegate);
}

class _DefaultSliverHeaderDelegate extends SliverHeaderDelegate {
  _DefaultSliverHeaderDelegate(this.childBuilder);

  final Function(
          BuildContext context, double shrinkOffset, bool overlapsContent)
      childBuilder;

  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return childBuilder.call(context, shrinkOffset, overlapsContent);
  }

  @override
  bool shouldRebuild(covariant SliverHeaderDelegate oldDelegate) {
    return true;
  }
}

/// A sliver whose size varies when the sliver is scrolled to the edge
/// of the viewport opposite the sliver's [GrowthDirection].
///
/// In the normal case of a [CustomScrollView] with no centered sliver, this
/// sliver will vary its size when scrolled to the leading edge of the viewport.
///
/// This is the layout primitive that [SliverAppBar] uses for its
/// shrinking/growing effect.
///
/// _To learn more about slivers, see [CustomScrollView.slivers]._
class SliverHeader extends StatelessWidget {
  /// Creates a sliver that varies its size when it is scrolled to the start of
  /// a viewport.
  ///
  /// The [delegate] and [pinned] arguments must not be null.
  const SliverHeader({
    super.key,
    required this.delegate,
    this.pinned = false,
  });

  SliverHeader.builder({
    Key? key,
    bool pinned = false,
    required Function(
            BuildContext context, double shrinkOffset, bool overlapsContent)
        childBuilder,
  }) : this(
          key: key,
          delegate: _DefaultSliverHeaderDelegate(childBuilder),
          pinned: pinned,
        );

  /// Configuration for the sliver's layout.
  ///
  /// The delegate provides the following information:
  ///
  ///  * The minimum and maximum dimensions of the sliver.
  ///
  ///  * The builder for generating the widgets of the sliver.
  ///
  ///  * The instructions for snapping the scroll offset, if [floating] is true.
  final SliverHeaderDelegate delegate;

  /// Whether to stick the header to the start of the viewport once it has
  /// reached its minimum size.
  ///
  /// If this is false, the header will continue scrolling off the screen after
  /// it has shrunk to its minimum extent.
  final bool pinned;

  @override
  Widget build(BuildContext context) {
    if (pinned) {
      return _SliverPinnedHeader(delegate: delegate);
    }
    return _SliverScrollingHeader(delegate: delegate);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(
      DiagnosticsProperty<SliverHeaderDelegate>(
        'delegate',
        delegate,
      ),
    );
    final List<String> flags = <String>[
      if (pinned) 'pinned',
    ];
    if (flags.isEmpty) {
      flags.add('normal');
    }
    properties.add(IterableProperty<String>('mode', flags));
  }
}

class _SliverHeaderElement extends RenderObjectElement {
  _SliverHeaderElement(_SliverHeaderRenderObjectWidget super.widget);

  @override
  _RenderSliverHeaderForWidgetsMixin get renderObject =>
      super.renderObject as _RenderSliverHeaderForWidgetsMixin;

  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    renderObject._element = this;
  }

  @override
  void unmount() {
    renderObject._element = null;
    super.unmount();
  }

  @override
  void update(_SliverHeaderRenderObjectWidget newWidget) {
    final _SliverHeaderRenderObjectWidget oldWidget =
        widget as _SliverHeaderRenderObjectWidget;
    super.update(newWidget);
    final SliverHeaderDelegate newDelegate = newWidget.delegate;
    final SliverHeaderDelegate oldDelegate = oldWidget.delegate;
    if (newDelegate != oldDelegate &&
        (newDelegate.runtimeType != oldDelegate.runtimeType ||
            newDelegate.shouldRebuild(oldDelegate))) {
      renderObject.triggerRebuild();
    }
  }

  @override
  void performRebuild() {
    super.performRebuild();
    renderObject.triggerRebuild();
  }

  Element? child;

  void _build(double shrinkOffset, bool overlapsContent) {
    owner!.buildScope(this, () {
      final _SliverHeaderRenderObjectWidget sliverHeaderRenderObjectWidget =
          widget as _SliverHeaderRenderObjectWidget;
      child = updateChild(
        child,
        sliverHeaderRenderObjectWidget.delegate
            .build(this, shrinkOffset, overlapsContent),
        null,
      );
    });
  }

  @override
  void forgetChild(Element child) {
    assert(child == this.child);
    this.child = null;
    super.forgetChild(child);
  }

  @override
  void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
    assert(renderObject.debugValidateChild(child));
    renderObject.child = child;
  }

  @override
  void moveRenderObjectChild(
      covariant RenderObject child, Object? oldSlot, Object? newSlot) {
    assert(false);
  }

  @override
  void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
    renderObject.child = null;
  }

  @override
  void visitChildren(ElementVisitor visitor) {
    if (child != null) {
      visitor(child!);
    }
  }
}

abstract class _SliverHeaderRenderObjectWidget extends RenderObjectWidget {
  const _SliverHeaderRenderObjectWidget({
    required this.delegate,
  });

  final SliverHeaderDelegate delegate;

  @override
  _SliverHeaderElement createElement() => _SliverHeaderElement(this);

  @override
  _RenderSliverHeaderForWidgetsMixin createRenderObject(BuildContext context);

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description.add(
      DiagnosticsProperty<SliverHeaderDelegate>(
        'delegate',
        delegate,
      ),
    );
  }
}

mixin _RenderSliverHeaderForWidgetsMixin on RenderSliverHeader {
  _SliverHeaderElement? _element;

  @override
  void updateChild(double shrinkOffset, bool overlapsContent) {
    assert(_element != null);
    _element!._build(shrinkOffset, overlapsContent);
  }

  @protected
  void triggerRebuild() {
    markNeedsLayout();
  }
}

class _SliverScrollingHeader extends _SliverHeaderRenderObjectWidget {
  const _SliverScrollingHeader({
    required super.delegate,
  });

  @override
  _RenderSliverHeaderForWidgetsMixin createRenderObject(BuildContext context) {
    return _RenderSliverScrollingHeaderForWidgets();
  }
}

class _RenderSliverScrollingHeaderForWidgets extends RenderSliverScrollingHeader
    with _RenderSliverHeaderForWidgetsMixin {
  _RenderSliverScrollingHeaderForWidgets();
}

class _SliverPinnedHeader extends _SliverHeaderRenderObjectWidget {
  const _SliverPinnedHeader({
    required super.delegate,
  });

  @override
  _RenderSliverHeaderForWidgetsMixin createRenderObject(BuildContext context) {
    return _RenderSliverPinnedHeaderForWidgets();
  }
}

class _RenderSliverPinnedHeaderForWidgets extends RenderSliverPinnedHeader
    with _RenderSliverHeaderForWidgetsMixin {
  _RenderSliverPinnedHeaderForWidgets();
}

/// A base class for slivers that have a [RenderBox] child which scrolls
/// normally, except that when it hits the leading edge (typically the top) of
/// the viewport
///
/// This class primarily provides helpers for managing the child, in particular:
///
///  * [layoutChild], which applies min and max extents and a scroll offset to
///    lay out the child. This is normally called from [performLayout].
///
///  * [childExtent], to convert the child's box layout dimensions to the sliver
///    geometry model.
///
///  * hit testing, painting, and other details of the sliver protocol.
///
/// Subclasses must implement [performLayout], and typically also will implement [updateChild].
abstract class RenderSliverHeader extends RenderSliver
    with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
  /// Creates a sliver that changes its size when scrolled to the start of the
  /// viewport.
  ///
  /// This is an abstract class; this constructor only initializes the [child].
  RenderSliverHeader({RenderBox? child}) {
    this.child = child;
  }

  /// The dimension of the child in the main axis.
  @protected
  double get childExtent {
    if (child == null) {
      return 0.0;
    }
    assert(child!.hasSize);
    switch (constraints.axis) {
      case Axis.vertical:
        return child!.size.height;
      case Axis.horizontal:
        return child!.size.width;
    }
  }

  bool _needsUpdateChild = true;
  double _lastShrinkOffset = 0.0;
  bool _lastOverlapsContent = false;

  /// Update the child render object if necessary.
  ///
  /// Called before the first layout, any time [markNeedsLayout] is called, and
  /// any time the scroll offset changes. The `shrinkOffset` is the header scroll
  /// offset. Zero means the header is fully expanded, any greater number means
  /// that the header has been scrolled by that much. The `overlapsContent` argument
  /// is true if the sliver's leading edge is beyond its normal place in the viewport
  /// contents, and false otherwise.
  ///
  /// When this method is called by [layoutChild], the [child] can be set,
  /// mutated, or replaced. (It should not be called outside [layoutChild].)
  ///
  /// Any time this method would mutate the child, call [markNeedsLayout].
  @protected
  void updateChild(double shrinkOffset, bool overlapsContent) {}

  @override
  void markNeedsLayout() {
    // This is automatically called whenever the child's intrinsic dimensions
    // change, at which point we should remeasure them during the next layout.
    _needsUpdateChild = true;
    super.markNeedsLayout();
  }

  /// Lays out the [child].
  ///
  /// This is called by [performLayout]. It applies the given `scrollOffset`
  /// (which need not match the offset given by the [constraints]) and the
  /// `maxExtent` (which need not match the value returned by the [maxExtent]
  /// getter).
  ///
  /// The `overlapsContent` argument is passed to [updateChild].
  @protected
  void layoutChild(double scrollOffset, {bool overlapsContent = false}) {
    final double shrinkOffset = scrollOffset;
    if (_needsUpdateChild ||
        _lastShrinkOffset != shrinkOffset ||
        _lastOverlapsContent != overlapsContent) {
      invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
        assert(constraints == this.constraints);
        updateChild(shrinkOffset, overlapsContent);
      });
      _lastShrinkOffset = shrinkOffset;
      _lastOverlapsContent = overlapsContent;
      _needsUpdateChild = false;
    }

    child?.layout(
      constraints.asBoxConstraints(),
      parentUsesSize: true,
    );
  }

  /// Returns the distance from the leading _visible_ edge of the sliver to the
  /// side of the child closest to that edge, in the scroll axis direction.
  ///
  /// For example, if the [constraints] describe this sliver as having an axis
  /// direction of [AxisDirection.down], then this is the distance from the top
  /// of the visible portion of the sliver to the top of the child. If the child
  /// is scrolled partially off the top of the viewport, then this will be
  /// negative. On the other hand, if the [constraints] describe this sliver as
  /// having an axis direction of [AxisDirection.up], then this is the distance
  /// from the bottom of the visible portion of the sliver to the bottom of the
  /// child. In both cases, this is the direction of increasing
  /// [SliverConstraints.scrollOffset].
  ///
  /// Calling this when the child is not visible is not valid.
  ///
  /// The argument must be the value of the [child] property.
  ///
  /// This must be implemented by [RenderSliverPersistentHeader] subclasses.
  ///
  /// If there is no child, this should return 0.0.
  @override
  double childMainAxisPosition(covariant RenderObject child) =>
      super.childMainAxisPosition(child);

  @override
  bool hitTestChildren(SliverHitTestResult result,
      {required double mainAxisPosition, required double crossAxisPosition}) {
    assert(geometry!.hitTestExtent > 0.0);
    if (child != null) {
      return hitTestBoxChild(BoxHitTestResult.wrap(result), child!,
          mainAxisPosition: mainAxisPosition,
          crossAxisPosition: crossAxisPosition);
    }
    return false;
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {
    assert(child == this.child);
    applyPaintTransformForBoxChild(child as RenderBox, transform);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null && geometry!.visible) {
      switch (applyGrowthDirectionToAxisDirection(
          constraints.axisDirection, constraints.growthDirection)) {
        case AxisDirection.up:
          offset += Offset(
              0.0,
              geometry!.paintExtent -
                  childMainAxisPosition(child!) -
                  childExtent);
          break;
        case AxisDirection.down:
          offset += Offset(0.0, childMainAxisPosition(child!));
          break;
        case AxisDirection.left:
          offset += Offset(
              geometry!.paintExtent -
                  childMainAxisPosition(child!) -
                  childExtent,
              0.0);
          break;
        case AxisDirection.right:
          offset += Offset(childMainAxisPosition(child!), 0.0);
          break;
      }
      context.paintChild(child!, offset);
    }
  }

  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config.addTagForChildren(RenderViewport.excludeFromScrolling);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty.lazy(
        'child position', () => childMainAxisPosition(child!)));
  }
}

/// A sliver with a [RenderBox] child which scrolls normally
///
/// This sliver makes no effort to avoid overlapping other content.
abstract class RenderSliverScrollingHeader extends RenderSliverHeader {
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// scrolls off.
  RenderSliverScrollingHeader({super.child});

  // Distance from our leading edge to the child's leading edge, in the axis
  // direction. Negative if we're scrolled off the top.
  double? _childPosition;

  /// Updates [geometry], and returns the new value for [childMainAxisPosition].
  ///
  /// This is used by [performLayout].
  @protected
  double updateGeometry() {
    final double paintExtent = childExtent - constraints.scrollOffset;
    geometry = SliverGeometry(
      scrollExtent: childExtent,
      paintOrigin: math.min(constraints.overlap, 0.0),
      paintExtent:
          clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
      maxPaintExtent: childExtent,
      hasVisualOverflow:
          true, // Conservatively say we do have overflow to avoid complexity.
    );
    return math.min(0.0, paintExtent - childExtent);
  }

  @override
  void performLayout() {
    final SliverConstraints constraints = this.constraints;
    layoutChild(constraints.scrollOffset);
    final double paintExtent = childExtent - constraints.scrollOffset;
    geometry = SliverGeometry(
      scrollExtent: childExtent,
      paintOrigin: math.min(constraints.overlap, 0.0),
      paintExtent:
          clampDouble(paintExtent, 0.0, constraints.remainingPaintExtent),
      maxPaintExtent: childExtent,
      hasVisualOverflow:
          true, // Conservatively say we do have overflow to avoid complexity.
    );
    _childPosition = updateGeometry();
  }

  @override
  double childMainAxisPosition(RenderBox child) {
    assert(child == this.child);
    assert(_childPosition != null);
    return _childPosition!;
  }
}

/// A sliver with a [RenderBox] child which never scrolls off the viewport in
/// the positive scroll direction, and scrolls on at a full size.
///
/// This sliver avoids overlapping other earlier slivers where possible.
abstract class RenderSliverPinnedHeader extends RenderSliverHeader {
  /// Creates a sliver that shrinks when it hits the start of the viewport, then
  /// stays pinned there.
  RenderSliverPinnedHeader({super.child});

  @override
  void performLayout() {
    final SliverConstraints constraints = this.constraints;
    final bool overlapsContent = constraints.overlap > 0.0;
    layoutChild(constraints.scrollOffset, overlapsContent: overlapsContent);
    final double effectiveRemainingPaintExtent =
        math.max(0, constraints.remainingPaintExtent - constraints.overlap);
    final double layoutExtent = clampDouble(
        childExtent - constraints.scrollOffset,
        0.0,
        effectiveRemainingPaintExtent);
    geometry = SliverGeometry(
      scrollExtent: childExtent,
      paintOrigin: constraints.overlap,
      paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
      layoutExtent: layoutExtent,
      maxPaintExtent: childExtent,
      maxScrollObstructionExtent: childExtent,
      cacheExtent: layoutExtent > 0.0
          ? -constraints.cacheOrigin + layoutExtent
          : layoutExtent,
      hasVisualOverflow:
          true, // Conservatively say we do have overflow to avoid complexity.
    );
  }

  @override
  double childMainAxisPosition(RenderBox child) => 0.0;

  @override
  void showOnScreen({
    RenderObject? descendant,
    Rect? rect,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
    final Rect? localBounds = descendant != null
        ? MatrixUtils.transformRect(
            descendant.getTransformTo(this), rect ?? descendant.paintBounds)
        : rect;

    Rect? newRect;
    switch (applyGrowthDirectionToAxisDirection(
        constraints.axisDirection, constraints.growthDirection)) {
      case AxisDirection.up:
        newRect = _trim(localBounds, bottom: childExtent);
        break;
      case AxisDirection.right:
        newRect = _trim(localBounds, left: 0);
        break;
      case AxisDirection.down:
        newRect = _trim(localBounds, top: 0);
        break;
      case AxisDirection.left:
        newRect = _trim(localBounds, right: childExtent);
        break;
    }

    super.showOnScreen(
      descendant: this,
      rect: newRect,
      duration: duration,
      curve: curve,
    );
  }
}

Rect? _trim(
  Rect? original, {
  double top = -double.infinity,
  double right = double.infinity,
  double bottom = double.infinity,
  double left = -double.infinity,
}) =>
    original?.intersect(Rect.fromLTRB(left, top, right, bottom));
